import { appendFileSync, existsSync, rmSync, statSync } from "fs";
import * as path from "path";
import { Diagnostic } from "../diagnostic";
import { readFileSync } from "../../common/system";
import { hasProperty } from "../../common/util";
import { DefaultOptions, MDMLoaderOptions, MDMSerializerOptions, Mutable } from "../../common/types";
import { BaseMDMDocument } from "./impl";
import MDMLoader from "./loader";
import Serializer from "./serializer";
import * as t from "./types";
import { serializeXmlToString } from "./xml";

function isMDMSerializerExportFiles(obj?: any): obj is t.MDMSerializerExportFiles {
    return obj && hasProperty(obj, "scriptFile") && hasProperty(obj, "routingFile");
}

export class MDMDocument extends BaseMDMDocument implements t.MDMDocument {

    private readonly _serializer: t.Serializer;
    private readonly _loader: t.MDMLoader;
    private _options: { loader: MDMLoaderOptions, serializer: MDMSerializerOptions };

    public constructor(options?: { loader: MDMLoaderOptions, serializer: MDMSerializerOptions }) {
        super();
        this._options = options ?? { loader: DefaultOptions.mdmLoaderOptions, serializer: DefaultOptions.mdmSerializerOptions };
        this._serializer = new Serializer(this._options.serializer);
        this._loader = new MDMLoader(this._options.loader);
        // initialize properties
        (this as Mutable<BaseMDMDocument>).versions = this._loader.factory.createVersions(this, this);
        (this as Mutable<BaseMDMDocument>).currentVersion = "";
        (this as Mutable<BaseMDMDocument>).mdmVersion = this._options.loader.mdmVersion;
        (this as Mutable<BaseMDMDocument>).pages = this._loader.factory.createPages(this, "@pages");
        (this as Mutable<BaseMDMDocument>).createdByVersion = this._options.loader.mdmVersion;
        (this as Mutable<BaseMDMDocument>).lastUpdatedByVersion = this._options.loader.mdmVersion;
        (this as Mutable<BaseMDMDocument>).systemRouting = this._loader.factory.createRoutings(this);
        (this as Mutable<BaseMDMDocument>).routings = this._loader.factory.createRoutings(this);
        (this as Mutable<BaseMDMDocument>).categoryMap = this._loader.factory.createCategoryMap(this);
        (this as Mutable<BaseMDMDocument>).aliasMap = this._loader.factory.createAliasMap();
        (this as Mutable<BaseMDMDocument>).dataSources = this._loader.factory.createDataSources();
        (this as Mutable<BaseMDMDocument>).languages = this._loader.factory.createLanguages(this, this._options.loader.defaultLanguage.longCode, this._options.loader.defaultLanguage.longCode);
        (this as Mutable<BaseMDMDocument>).contexts = this._loader.factory.createContexts(this, this._options.loader.defaultContext, this._options.loader.defaultContext);
        (this as Mutable<BaseMDMDocument>).labelTypes = this._loader.factory.createContexts(this, this._options.loader.defaultLabelType, this._options.loader.defaultLabelType);
        (this as Mutable<BaseMDMDocument>).routingContexts = this._loader.factory.createContexts(this, this._options.loader.defaultRoutingContext, this._options.loader.defaultRoutingContext);
        (this as Mutable<BaseMDMDocument>).scriptTypes = this._loader.factory.createContexts(this, this._options.loader.defaultScriptType, this._options.loader.defaultScriptType);
    }

    public serializeToScript(options?: MDMSerializerOptions): string {
        return this._serializer.serializeToString(this, options);
    }

    public serializeToXml(options?: MDMSerializerOptions): Document {
        return this._serializer.serializeToXml(this, options);
    }

    public updateFromScript(script: string): void {
        throw new Error("Method not implemented.");
    }

    public save(destination?: string | undefined): void {
        const doc = this._serializer.serializeToXml(this);
        const content = serializeXmlToString(doc);

        let url  = destination ?? this.url;

        if (!path.isAbsolute(url)) {
            throw new Error(`path '${url}' is not an absolute path.`);
        }

        if (existsSync(url) && statSync(url).isFile()) {
            rmSync(url);
        }

        appendFileSync(url, content);
    }

    public open(path: string): readonly Diagnostic[] {
        const text = readFileSync(path);
        if (text.length === 0) {
            return [];
        }
        this._loader.clear();
        this._loader.loadTo(this, path, text);
        return this._loader.xmlErrors;
    }

    public close(): void {
        this._loader.clear();
    }

    public exportSerializedFile(destination: t.MDMSerializerExportFiles, options?: MDMSerializerOptions): void;
    public exportSerializedFile(options?: MDMSerializerOptions): t.MDMSerializerExportFiles;
    public exportSerializedFile(arg1?: t.MDMSerializerExportFiles | MDMSerializerOptions, arg2?: MDMSerializerOptions): t.MDMSerializerExportFiles {

        let scriptFile = "";
        let routingFile = "";
        let fileName = path.basename(this.url, ".mdd");
        let options: MDMSerializerOptions | undefined;

        if (isMDMSerializerExportFiles(arg1)) {
            scriptFile = arg1.scriptFile;
            routingFile = arg1.routingFile;
            options = arg2;
        }
        else {
            scriptFile = path.resolve(this.projectDirectory, fileName + ".m.dms");
            routingFile = path.resolve(this.projectDirectory, fileName + ".r.dms");
            options = arg1;
        }

        if (existsSync(scriptFile)) {
            rmSync(scriptFile);
        }
        appendFileSync(scriptFile, this.serializeToScript(options));

        if (existsSync(routingFile)) {
            rmSync(routingFile);
        }

        if (this.routings.scripts.length > 0 && this.routings.scripts[0].length > 0) {
            let script = this.routings.scripts[0][0].script;
            if (script && script.length > 2) {
                appendFileSync(routingFile, `Routing(${this.routings.scripts[0].context})\n\n${script.substring(1, script.length - 2)}\n\nEnd Routing`);
            }
        }

        return { scriptFile, routingFile };
    }

}

